/*
 * Title:        CloudSim Toolkit
 * Description:  CloudSim (Cloud Simulation) Toolkit for Modeling and Simulation of Clouds
 * Licence:      GPL - http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2009-2012, The University of Melbourne, Australia
 */

package cloudsim;

import cloudsim.distributions.ContinuousDistribution;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * An implementation of a storage system. It simulates the behavior of a typical hard drive storage.
 * The default values for this storage are those of a "Maxtor DiamonMax 10 ATA" hard disk with the
 * following parameters:
 * <ul>
 * <li>latency = 4.17 ms</li>
 * <li>avg seek time = 9 m/s</li>
 * <li>max transfer rate = 133 MB/sec</li>
 * </ul>
 *
 * @author Uros Cibej
 * @author Anthony Sulistio
 * @since CloudSim Toolkit 1.0
 */
public class HarddriveStorage implements Storage {

    /**
     * A list storing the names of all files on the hard drive.
     */
    private List<String> nameList;

    /**
     * A list storing all files stored on the hard drive.
     */
    private List<File> fileList;

    /**
     * The name of the hard drive.
     */
    private final String name;

    /**
     * A generator required to randomize the seek time.
     */
    private ContinuousDistribution gen;

    /**
     * The current size of files on the hard drive.
     */
    private double currentSize;

    /**
     * The total capacity of the hard drive in MB.
     */
    private final double capacity;

    /**
     * The maximum transfer rate in MB/sec.
     */
    private double maxTransferRate;

    /**
     * The latency of the hard drive in seconds.
     */
    private double latency;

    /**
     * The average seek time in seconds.
     */
    private double avgSeekTime;

    /**
     * Creates a new hard drive storage with a given name and capacity.
     *
     * @param name     the name of the new hard drive storage
     * @param capacity the capacity in MByte
     * @throws ParameterException when the name and the capacity are not valid
     */
    public HarddriveStorage(String name, double capacity) throws ParameterException {
        if (name == null || name.length() == 0) {
            throw new ParameterException("HarddriveStorage(): Error - invalid storage name.");
        }

        if (capacity <= 0) {
            throw new ParameterException("HarddriveStorage(): Error - capacity <= 0.");
        }

        this.name = name;
        this.capacity = capacity;
        init();
    }

    /**
     * Creates a new hard drive storage with a given capacity. In this case the name of the storage
     * is a default name.
     *
     * @param capacity the capacity in MByte
     * @throws ParameterException when the capacity is not valid
     */
    public HarddriveStorage(double capacity) throws ParameterException {
        if (capacity <= 0) {
            throw new ParameterException("HarddriveStorage(): Error - capacity <= 0.");
        }
        name = "HarddriveStorage";
        this.capacity = capacity;
        init();
    }

    /**
     * The initialization of the hard drive is done in this method. The most common parameters, such
     * as latency, average seek time and maximum transfer rate are set. The default values are set
     * to simulate the "Maxtor DiamonMax 10 ATA" hard disk. Furthermore, the necessary lists are
     * created.
     */
    private void init() {
        fileList = new ArrayList<File>();
        nameList = new ArrayList<String>();
        gen = null;
        currentSize = 0;

        latency = 0.00417;     // 4.17 ms in seconds
        avgSeekTime = 0.009;   // 9 ms
        maxTransferRate = 133; // in MB/sec
    }

    @Override
    public double getAvailableSpace() {
        return capacity - currentSize;
    }

    @Override
    public boolean isFull() {
        if (Math.abs(currentSize - capacity) < .0000001) { // currentSize == capacity
            return true;
        }
        return false;
    }

    @Override
    public int getNumStoredFile() {
        return fileList.size();
    }

    @Override
    public boolean reserveSpace(int fileSize) {
        if (fileSize <= 0) {
            return false;
        }

        if (currentSize + fileSize >= capacity) {
            return false;
        }

        currentSize += fileSize;
        return true;
    }

    @Override
    public double addReservedFile(File file) {
        if (file == null) {
            return 0;
        }

        currentSize -= file.getSize();
        double result = addFile(file);

        // if add file fails, then set the current size back to its old value
        if (result == 0.0) {
            currentSize += file.getSize();
        }

        return result;
    }

    @Override
    public boolean hasPotentialAvailableSpace(int fileSize) {
        if (fileSize <= 0) {
            return false;
        }

        // check if enough space left
        if (getAvailableSpace() > fileSize) {
            return true;
        }

        Iterator<File> it = fileList.iterator();
        File file = null;
        int deletedFileSize = 0;

        // if not enough space, then if want to clear/delete some files
        // then check whether it still have space or not
        boolean result = false;
        while (it.hasNext()) {
            file = it.next();
            if (!file.isReadOnly()) {
                deletedFileSize += file.getSize();
            }

            if (deletedFileSize > fileSize) {
                result = true;
                break;
            }
        }

        return result;
    }

    @Override
    public double getCapacity() {
        return capacity;
    }

    @Override
    public double getCurrentSize() {
        return currentSize;
    }

    @Override
    public String getName() {
        return name;
    }

    /**
     * Sets the latency of this hard drive in seconds.
     *
     * @param latency the new latency in seconds
     * @return <tt>true</tt> if the setting succeeded, <tt>false</tt> otherwise
     */
    public boolean setLatency(double latency) {
        if (latency < 0) {
            return false;
        }

        this.latency = latency;
        return true;
    }

    /**
     * Gets the latency of this hard drive in seconds.
     *
     * @return the latency in seconds
     */
    public double getLatency() {
        return latency;
    }

    @Override
    public boolean setMaxTransferRate(int rate) {
        if (rate <= 0) {
            return false;
        }

        maxTransferRate = rate;
        return true;
    }

    @Override
    public double getMaxTransferRate() {
        return maxTransferRate;
    }

    /**
     * Sets the average seek time of the storage in seconds.
     *
     * @param seekTime the average seek time in seconds
     * @return <tt>true</tt> if the setting succeeded, <tt>false</tt> otherwise
     */
    public boolean setAvgSeekTime(double seekTime) {
        return setAvgSeekTime(seekTime, null);
    }

    /**
     * Sets the average seek time and a new generator of seek times in seconds. The generator
     * determines a randomized seek time.
     *
     * @param seekTime the average seek time in seconds
     * @param gen      the ContinuousGenerator which generates seek times
     * @return <tt>true</tt> if the setting succeeded, <tt>false</tt> otherwise
     */
    public boolean setAvgSeekTime(double seekTime, ContinuousDistribution gen) {
        if (seekTime <= 0.0) {
            return false;
        }

        avgSeekTime = seekTime;
        this.gen = gen;
        return true;
    }

    /**
     * Gets the average seek time of the hard drive in seconds.
     *
     * @return the average seek time in seconds
     */
    public double getAvgSeekTime() {
        return avgSeekTime;
    }

    @Override
    public File getFile(String fileName) {
        // check first whether file name is valid or not
        File obj = null;
        if (fileName == null || fileName.length() == 0) {
            Log.printConcatLine(name, ".getFile(): Warning - invalid " + "file name.");
            return obj;
        }

        Iterator<File> it = fileList.iterator();
        int size = 0;
        int index = 0;
        boolean found = false;
        File tempFile = null;

        // find the file in the disk
        while (it.hasNext()) {
            tempFile = it.next();
            size += tempFile.getSize();
            if (tempFile.getName().equals(fileName)) {
                found = true;
                obj = tempFile;
                break;
            }

            index++;
        }

        // if the file is found, then determine the time taken to get it
        if (found) {
            obj = fileList.get(index);
            double seekTime = getSeekTime(size);
            double transferTime = getTransferTime(obj.getSize());

            // total time for this operation
            obj.setTransactionTime(seekTime + transferTime);
        }

        return obj;
    }

    @Override
    public List<String> getFileNameList() {
        return nameList;
    }

    /**
     * Get the seek time for a file with the defined size. Given a file size in MB, this method
     * returns a seek time for the file in seconds.
     *
     * @param fileSize the size of a file in MB
     * @return the seek time in seconds
     */
    private double getSeekTime(int fileSize) {
        double result = 0;

        if (gen != null) {
            result += gen.sample();
        }

        if (fileSize > 0 && capacity != 0) {
            result += (fileSize / capacity);
        }

        return result;
    }

    /**
     * Gets the transfer time of a given file.
     *
     * @param fileSize the size of the transferred file
     * @return the transfer time in seconds
     */
    private double getTransferTime(int fileSize) {
        double result = 0;
        if (fileSize > 0 && capacity != 0) {
            result = (fileSize * maxTransferRate) / capacity;
        }

        return result;
    }

    /**
     * Check if the file is valid or not. This method checks whether the given file or the file name
     * of the file is valid. The method name parameter is used for debugging purposes, to output in
     * which method an error has occurred.
     *
     * @param file       the file to be checked for validity
     * @param methodName the name of the method in which we check for validity of the file
     * @return <tt>true</tt> if the file is valid, <tt>false</tt> otherwise
     */
    private boolean isFileValid(File file, String methodName) {

        if (file == null) {
            Log.printConcatLine(name, ".", methodName, ": Warning - the given file is null.");
            return false;
        }

        String fileName = file.getName();
        if (fileName == null || fileName.length() == 0) {
            Log.printConcatLine(name, "." + methodName, ": Warning - invalid file name.");
            return false;
        }

        return true;
    }

    /**
     * {@inheritDoc}
     * <p>
     * <p/>First, the method checks if there is enough space on the storage,
     * then it checks if the file with the same name is already taken to avoid duplicate filenames.
     *
     * @param file {@inheritDoc}
     * @return {@inheritDoc}
     */
    @Override
    public double addFile(File file) {
        double result = 0.0;
        // check if the file is valid or not
        if (!isFileValid(file, "addFile()")) {
            return result;
        }

        // check the capacity
        if (file.getSize() + currentSize > capacity) {
            Log.printConcatLine(name, ".addFile(): Warning - not enough space to store ", file.getName());
            return result;
        }

        // check if the same file name is alredy taken
        if (!contains(file.getName())) {
            double seekTime = getSeekTime(file.getSize());
            double transferTime = getTransferTime(file.getSize());

            fileList.add(file);               // add the file into the HD
            nameList.add(file.getName());     // add the name to the name list
            currentSize += file.getSize();    // increment the current HD size
            result = seekTime + transferTime;  // add total time
        }
        file.setTransactionTime(result);
        return result;
    }

    @Override
    public double addFile(List<File> list) {
        double result = 0.0;
        if (list == null || list.isEmpty()) {
            Log.printConcatLine(name, ".addFile(): Warning - list is empty.");
            return result;
        }

        Iterator<File> it = list.iterator();
        File file = null;
        while (it.hasNext()) {
            file = it.next();
            result += addFile(file);    // add each file in the list
        }
        return result;
    }

    @Override
    public File deleteFile(String fileName) {
        if (fileName == null || fileName.length() == 0) {
            return null;
        }

        Iterator<File> it = fileList.iterator();
        File file = null;
        while (it.hasNext()) {
            file = it.next();
            String name = file.getName();

            // if a file is found then delete
            if (fileName.equals(name)) {
                double result = deleteFile(file);
                file.setTransactionTime(result);
                break;
            } else {
                file = null;
            }
        }
        return file;
    }

    @Override
    public double deleteFile(String fileName, File file) {
        return deleteFile(file);
    }

    @Override
    public double deleteFile(File file) {
        double result = 0.0;
        // check if the file is valid or not
        if (!isFileValid(file, "deleteFile()")) {
            return result;
        }
        double seekTime = getSeekTime(file.getSize());
        double transferTime = getTransferTime(file.getSize());

        // check if the file is in the storage
        if (contains(file)) {
            fileList.remove(file);            // remove the file HD
            nameList.remove(file.getName());  // remove the name from name list
            currentSize -= file.getSize();    // decrement the current HD space
            result = seekTime + transferTime;  // total time
            file.setTransactionTime(result);
        }
        return result;
    }

    @Override
    public boolean contains(String fileName) {
        boolean result = false;
        if (fileName == null || fileName.length() == 0) {
            Log.printConcatLine(name, ".contains(): Warning - invalid file name");
            return result;
        }
        // check each file in the list
        Iterator<String> it = nameList.iterator();
        while (it.hasNext()) {
            String name = it.next();
            if (name.equals(fileName)) {
                result = true;
                break;
            }
        }
        return result;
    }

    @Override
    public boolean contains(File file) {
        boolean result = false;
        if (!isFileValid(file, "contains()")) {
            return result;
        }

        result = contains(file.getName());
        return result;
    }

    @Override
    public boolean renameFile(File file, String newName) {
        // check whether the new filename is conflicting with existing ones
        // or not
        boolean result = false;
        if (contains(newName)) {
            return result;
        }

        // replace the file name in the file (physical) list
        File obj = getFile(file.getName());
        if (obj == null) {
            return result;
        } else {
            obj.setName(newName);
        }

        // replace the file name in the name list
        Iterator<String> it = nameList.iterator();
        while (it.hasNext()) {
            String name = it.next();
            if (name.equals(file.getName())) {
                file.setTransactionTime(0);
                nameList.remove(name);
                nameList.add(newName);
                result = true;
                break;
            }
        }

        return result;
    }

}
